package com.tianlu.store.utils;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.qcloud.cos.COSClient;
import com.qcloud.cos.ClientConfig;
import com.qcloud.cos.auth.BasicCOSCredentials;
import com.qcloud.cos.auth.COSCredentials;
import com.qcloud.cos.http.HttpMethodName;
import com.qcloud.cos.http.HttpProtocol;
import com.qcloud.cos.model.ObjectMetadata;
import com.qcloud.cos.model.PutObjectRequest;
import com.qcloud.cos.model.UploadResult;
import com.qcloud.cos.region.Region;
import com.qcloud.cos.transfer.TransferManager;
import com.qcloud.cos.transfer.TransferManagerConfiguration;
import com.qcloud.cos.transfer.Upload;
import com.tianlu.store.common.IdGen;
import com.tianlu.store.common.ResultCode;
import com.tianlu.store.common.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 腾讯云文件工具类
 *
 * @author liangdl
 * @since 2022/7/22 18:10
 */
@Slf4j
@Component
public class CosFileUtils {
    /**
     * 秘钥ID
     */
    private static final String SECRET_ID = "AKIDTmtmRu9E7nbDbkhPt9B2g6HTQ8cr284n";

    /**
     * 秘钥key
     */
    private static final String SECRET_KEY = "YszRFrFOJzRfeOwGLBnCNNBl7F9pIX7V";

    /**
     * 所属地域
     */
    private static final String REGION_NAME = "ap-guangzhou";

    /**
     * 存储桶名称
     */
    private static final String BUCKET_NAME = "tianlu-1317527946";

    /**
     * 日期格式化
     */
    private static final String DATE_FORMAT_STR = "yyyyMM";

    /**
     * 线程数
     */
    private static final Integer POOL_SIZE = 32;
    /**
     * 队列初始化大小
     **/
    private static final int CAPACITY = 10;

    /**
     * 分块上传阈值
     */
    private static final long THRESHOLD = 10 * 1024 * 1024;

    /**
     * 分块大小
     */
    private static final long PART_SIZE = 10 * 1024 * 1024;

    /**
     * 空闲线程存活时间
     **/
    private static final long KEEP_ALIVE_TIME = 0L;

    /**
     * 文件全路径
     */
    private static final String PATH_FORMAT = "https://{0}.cos.ap-guangzhou.myqcloud.com/{1}";

    /**
     * 域名
     */
    private static final String DOMAIN_NAME = ".com";

    /**
     * 问号
     **/
    public static final String COMMON_QUESTION_MARK = "?";

    /**
     * 转义问号
     **/
    public static final String SPLIT_QUESTION_MARK = "\\?";

    /**
     * 下载文件
     *
     * @param path           文件地址
     * @param expirationTime 过期时间（时间戳）
     * @return String  授权后下载地址
     * @author liangdl
     * @since 2022/7/23 11:21
     */
    public static String download(String path, Long expirationTime) {
        // 校验路径
        if (StrUtil.isBlank(path) || !path.contains(DOMAIN_NAME)) {
            throw new ServiceException(ResultCode.CODE_400, null, "下载路径有误！");
        }

        // 截取文件路径
        String bucketName = path.substring(path.indexOf("//") + 2, path.indexOf("."));
        String key = path.substring(path.indexOf(DOMAIN_NAME) + 5, path.indexOf(COMMON_QUESTION_MARK));

        // 生成cos客户端。
        COSClient cosClient = createCosClient();

        // 下载链接过期时间，这里默认两分钟后过期
        expirationTime = expirationTime == null ? 120000 : expirationTime;
        Date expirationDate = new Date(System.currentTimeMillis() + expirationTime);

        // 生成授权链接
        Map<String, String> params = new HashMap<>(0);
        Map<String, String> headers = new HashMap<>(0);
        HttpMethodName method = HttpMethodName.GET;
        URL url = cosClient.generatePresignedUrl(bucketName, key, expirationDate, method, headers, params);
        return url.toString();
    }

    /**
     * 批量下载文件
     *
     * @param urlList        文件地址
     * @param expirationTime 过期时间（时间戳）
     * @return List<String>  授权后下载地址
     * @author liangdl
     * @since 2022/9/20 11:21
     */
    public static void batchDownload(HttpServletResponse response, List<String> urlList, Long expirationTime) {
        // 校验路径
        if (CollectionUtil.isEmpty(urlList)) {
            throw new ServiceException(ResultCode.CODE_400, null, "下载路径不能为空！");
        }

        // 生成cos客户端。
        COSClient cosClient = createCosClient();

        // 下载链接过期时间，这里默认两分钟后过期
        expirationTime = expirationTime == null ? 120000 : expirationTime;
        Date expirationDate = new Date(System.currentTimeMillis() + expirationTime);

        // 生成授权链接
        Map<String, String> params = new HashMap<>(0);
        Map<String, String> headers = new HashMap<>(0);
        HttpMethodName method = HttpMethodName.GET;

        List<String> pathList = new ArrayList<>(urlList.size());
        Map<String, String> pathNameMap = new HashMap<>(urlList.size());
        for (String path : urlList) {
            // 截取文件路径
            String bucketName = path.substring(path.indexOf("//") + 2, path.indexOf("."));
            String key = path.substring(path.indexOf(DOMAIN_NAME) + 5, path.indexOf(COMMON_QUESTION_MARK));
            URL url = cosClient.generatePresignedUrl(bucketName, key, expirationDate, method, headers, params);
            pathList.add(url.toString());
            pathNameMap.put(url.toString(), path.split(SPLIT_QUESTION_MARK)[1]);
        }

        try {
            if (pathList.size() == 1) {
                //输出文件流
                writeFileToRes(response, pathNameMap.get(pathList.get(0)), pathList.get(0));
            } else if (pathList.size() > 1) {
                //压缩文件
                File file = compressedFileToZip(pathList, pathNameMap);
                //输出文件流
                writeFileToRes(response, file.getName(), "file:" + file.getAbsolutePath());
                //删除压缩包
                if (file.exists()) {
                    file.delete();
                }
            }
        } catch (IOException e) {
            throw new ServiceException(ResultCode.CODE_500, e.getMessage(), "文件下载失败！");
        }
    }

    /**
     * 压缩文件
     *
     * @param pathNameMap  文件名
     * @param filePathList 附件路径
     * @return File
     * @throws IOException
     */
    private static File compressedFileToZip(List<String> filePathList, Map<String, String> pathNameMap) throws IOException {
        //压缩包具体名称
        String zipFileName = IdGen.snowflakeId() + ".zip";
        //生成压缩包存储地址（最后会删掉）
        String fileZip = "../newDir/" + zipFileName;
        OutputStream os = null;
        ZipOutputStream zos = null;
        File file = new File(fileZip);
        try {
            if (!file.getParentFile().exists()) {
                file.getParentFile().mkdirs();
            }
            os = Files.newOutputStream(file.toPath());
            //压缩文件
            zos = new ZipOutputStream(os);
            byte[] buf = new byte[1024];
            for (String filePath : filePathList) {
                //直接在压缩包中添加文件
                zos.putNextEntry(new ZipEntry(pathNameMap.get(filePath)));
                int len;
                URL url = new URL(filePath);
                InputStream in = url.openStream();
                while ((len = in.read(buf)) != -1) {
                    zos.write(buf, 0, len);
                }
                zos.closeEntry();
                in.close();
            }
        } catch (Exception e) {
            log.error("文件打包失败：" + e.getMessage());
            throw e;
        } finally {
            //关闭流
            if (zos != null) {
                try {
                    zos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            //关闭流
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return file;
    }

    /**
     * 输出文件流到response
     *
     * @param response response
     * @param fileName fileName
     * @param fileUrl  fileUrl
     * @throws IOException IOException
     */
    public static void writeFileToRes(HttpServletResponse response, String fileName, String fileUrl) throws IOException {
        URL url = new URL(fileUrl);
        InputStream inputStream = url.openStream();
//        FileInputStream inputStream = new FileInputStream(file);
        // 1.设置文件ContentType类型，这样设置，会自动判断下载文件类型
        response.setContentType("application/octet-stream");
        // 2.设置文件头：最后一个参数是设置下载文件名
        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
//        response.addHeader("Content-Length", "" + file.length());

        // 3.通过response获取ServletOutputStream对象(out)
        ServletOutputStream out = response.getOutputStream();

        int b = 0;
        byte[] buffer = new byte[1024];
        while (b != -1) {
            b = inputStream.read(buffer);
            //4.写到输出流(out)中
            out.write(buffer, 0, b);
        }
        out.flush();
        out.close();
        inputStream.close();
    }

    /**
     * 上传文件
     *
     * @param files 文件
     * @return 文件路径
     * @author liangdl
     * @since 2022/7/22 19:20
     */
    public static String batchUpload(MultipartFile[] files) {
        // 使用高级接口必须先保证本进程存在一个 TransferManager 实例，如果没有则创建
        TransferManager transferManager = createTransferManager();

        // 获取当前日期作为文件夹
        SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT_STR);
        String dateStr = format.format(new Date());

        // 获取桶名称
        String bucketName = BUCKET_NAME;

        // 返回文件路径
        StringJoiner joiner = new StringJoiner("|");

        // 遍历上传文件
        for (MultipartFile multipartFile : files) {
            // 指定要上传的文件
            File file = multipartFileToFile(multipartFile);
            String fileName = multipartFile.getOriginalFilename();

            // 获取对象键(Key)是对象在存储桶中的唯一标识
            String prefix = fileName.substring(fileName.lastIndexOf("."));
            String key = dateStr + "/" + IdGen.uuid() + prefix;

            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, file);

            try {
                // 高级接口会返回一个异步结果Upload
                // 可同步地调用 waitForUploadResult 方法等待上传完成，成功返回UploadResult, 失败抛出异常
                Upload upload = transferManager.upload(putObjectRequest);
                UploadResult uploadResult = upload.waitForUploadResult();
            } catch (Exception e) {
                throw new ServiceException(ResultCode.CODE_500, e.getMessage(), "上传文件失败");
            }
            String path = MessageFormat.format(PATH_FORMAT, bucketName, key + COMMON_QUESTION_MARK + fileName);
            joiner.add(path);
        }

        // 确定本进程不再使用 transferManager 实例之后，关闭之
        // 指定参数为 true, 则同时会关闭 transferManager 内部的 COSClient 实例。
        // 指定参数为 false, 则不会关闭 transferManager 内部的 COSClient 实例。
        transferManager.shutdownNow(true);
        return joiner.toString();
    }

    /**
     * 创建 TransferManager 实例，这个实例用来后续调用高级接口
     *
     * @return TransferManager
     * @author liangdl
     * @since 2022/10/8 14:49
     */
    private static TransferManager createTransferManager() {
        // 创建一个 COSClient 实例，这是访问 COS 服务的基础实例
        COSClient cosClient = createCosClient();

        // 自定义线程池大小，建议在客户端与 COS 网络充足（例如使用腾讯云的 CVM，同地域上传 COS）的情况下，设置成16或32即可，可较充分的利用网络资源
        // 对于使用公网传输且网络带宽质量不高的情况，建议减小该值，避免因网速过慢，造成请求超时
        ExecutorService threadPool = new ThreadPoolExecutor(POOL_SIZE, POOL_SIZE,
                KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(CAPACITY), new ThreadFactoryBuilder().setNameFormat("cos-file-pool-%d").build(), new ThreadPoolExecutor.AbortPolicy());
        // 传入一个 thread-pool, 若不传入线程池，默认 TransferManager 中会生成一个单线程的线程池
        TransferManager transferManager = new TransferManager(cosClient, threadPool);
        // 设置高级接口的配置项
        // 分块上传阈值和分块大小分别为 5MB 和 1MB
        TransferManagerConfiguration transferManagerConfiguration = new TransferManagerConfiguration();
        transferManagerConfiguration.setMultipartUploadThreshold(THRESHOLD);
        transferManagerConfiguration.setMinimumUploadPartSize(PART_SIZE);
        transferManager.setConfiguration(transferManagerConfiguration);

        return transferManager;
    }

    /**
     * 生成cos客户端
     *
     * @return cos客户端
     * @author liangdl
     * @since 2022/8/2 10:11
     */
    private static COSClient createCosClient() {
        // 生成cos客户端
        COSCredentials cred = new BasicCOSCredentials(SECRET_ID, SECRET_KEY);
        Region region = new Region(REGION_NAME);
        ClientConfig clientConfig = new ClientConfig(region);
        clientConfig.setHttpProtocol(HttpProtocol.https);
        return new COSClient(cred, clientConfig);
    }

    /**
     * 将MultipartFile转换为File
     *
     * @param multiFile MultipartFile
     * @return File
     */
    public static File multipartFileToFile(MultipartFile multiFile) {
        // 获取文件名
        String fileName = multiFile.getOriginalFilename();
        if (StrUtil.isBlank(fileName)) {
            throw new ServiceException(ResultCode.CODE_400, null, "文件不存在！");
        }
        // 获取文件后缀
        String prefix = fileName.substring(fileName.lastIndexOf("."));
        try {
            File file = File.createTempFile(fileName, prefix);
            multiFile.transferTo(file);
            return file;
        } catch (Exception e) {
            log.error("MultipartFile转换为File转换异常：" + e.getMessage());
            throw new ServiceException(ResultCode.CODE_400, null, "MultipartFile转换为File转换异常！");
        }
    }

    /**
     * 将临时文件上传到服务器
     *
     * @param tempPath 临时地址
     * @return 上传后的地址
     */
    public static String uploadByPath(String tempPath) {
        try {
            URL url = new URL(tempPath);
            InputStream inputStream = url.openConnection().getInputStream();
            String fileName = tempPath.substring(tempPath.lastIndexOf("/"));
            return uploadInputStream(inputStream, fileName);
        } catch (Exception e) {
            log.error("上传图片失败：" + e.getMessage());
            return "";
        }
    }

    /**
     * 上传文件
     *
     * @param inputStream 文件流
     * @param fileName    文件名
     * @return 路径
     * @author liangdl
     * @since 2022/7/26 9:43
     */
    public static String uploadInputStream(InputStream inputStream, String fileName) {
        if (Objects.isNull(inputStream)) {
            throw new ServiceException(ResultCode.CODE_400, null, "文件不存在！");
        }

        // 生成cos客户端。
        COSClient cosClient = createCosClient();

        // 获取文件后缀
        String prefix = fileName.substring(fileName.lastIndexOf("."));
        String key = "QRCode" + "/" + IdGen.uuid() + prefix;
        // 指定文件上传到 COS 上的路径，即对象键。例如对象键为folder/picture.jpg，则表示将文件 picture.jpg 上传到 folder 路径下
        PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, key, inputStream, new ObjectMetadata());
        cosClient.putObject(putObjectRequest);
        return MessageFormat.format(PATH_FORMAT, BUCKET_NAME, key + COMMON_QUESTION_MARK + fileName);
    }

    /**
     * 上传文件
     *
     * @param inputStream 文件流
     * @param fileName    文件名
     * @return 路径
     * @author liangdl
     * @since 2022/7/26 9:43
     */
    public static String uploadQRCode(InputStream inputStream, String fileName) {
        if (Objects.isNull(inputStream)) {
            throw new ServiceException(ResultCode.CODE_400, null, "文件不存在！");
        }

        // 生成cos客户端。
        COSClient cosClient = createCosClient();

        // 获取文件后缀
        String key = "QRCode" + "/" + fileName;
        // 指定文件上传到 COS 上的路径，即对象键。例如对象键为folder/picture.jpg，则表示将文件 picture.jpg 上传到 folder 路径下
        PutObjectRequest putObjectRequest = new PutObjectRequest(BUCKET_NAME, key, inputStream, new ObjectMetadata());
        cosClient.putObject(putObjectRequest);
        return MessageFormat.format(PATH_FORMAT, BUCKET_NAME, key);
    }
}
